package css.core;
import java.util.ArrayList;
import java.util.Random;
import java.util.Timer;
import java.util.TimerTask;

import css.util.FileSaver;

/** 
 * This is the main body of the simulation program.
 * @author ZENG YI
 *
 */
public class MainSimulation implements Runnable{
	Timer simtimer=null;
	boolean stopped=false;
	int timer;
	int timelimit;
	int totalservicetime=0;
	int averageservicetime=0;
	int currentCustomerID; // The customer ID for the next incoming customer to the queue
    int prepackrate=10;
    int foodrate=10;
    int drinkrate=10;
    int fanddrate=10;
	int nextArrivalTime;
	int queueLength;
	int maxQueueLength;
	int maxWaitLength;
	int maxServiceTime;
	int queueLengthLimit;
	int nextPurchaseDoneTime;
	int totalTime;
	double arrivalRate;
	int numOfCashier;
	int numOfCook;
	int numOfBarista;
	int minCashier; 
	int maxCashier;
	int minCook;
	int maxCook;
	int minBar;
	int maxBar;
	int idlecashier;
	int idlecook;
	int idlebaristas;
	int samplerate=5;
	SimObserver observer=null;
	ArrayList<Customer> customerQueue = new ArrayList<Customer>();
	ArrayList<Customer> customerWaitingPool = new ArrayList<Customer>();
	ArrayList<Cashier> cashiers = new ArrayList<Cashier>();
	ArrayList<Cook> cooks = new ArrayList<Cook>();
	ArrayList<Barista> baristas = new ArrayList<Barista>();
	ArrayList<Order> orderQueue = new ArrayList<Order>();
	ArrayList<Food> pendingFoodQueue = new ArrayList<Food>();
	ArrayList<Drink> pendingDrinkQueue = new ArrayList<Drink>();
	Record log=new Record();
	
	public MainSimulation(double arrivalRate, int timeLimit,int prepackrate,int foodrate,int drinkrate,int fanddrate, int numOfCashier, int numOfCook, int numOfBarista,
							int minCashier, int maxCashier, int minCook, int maxCook, int minBar, int maxBar,int maxqueuelength){
		
		//Initialize parameters from user input
		simtimer=new Timer();
		
		this.timer = 0;
		this.currentCustomerID = 1;
		this.queueLength = 0;
		this.maxQueueLength = 0;
		this.maxWaitLength=0;
		this.maxServiceTime = 0;
		this.queueLengthLimit=maxqueuelength;
		this.arrivalRate = arrivalRate / 60;
		this.totalTime = timeLimit;
		this.prepackrate=prepackrate;
		this.foodrate=foodrate;
		this.drinkrate=drinkrate;
		this.fanddrate=fanddrate;
		this.idlecashier=this.numOfCashier = numOfCashier;
		this.idlecook=this.numOfCook = numOfCook;
		this.idlebaristas=this.numOfBarista = numOfBarista;
		this.minCashier=minCashier;
		this.maxCashier=maxCashier;
		this.minCook=minCook;
		this.maxCook=maxCook;
		this.minBar=minBar;
		this.maxBar=maxBar;
	}
	
	class simtask extends TimerTask{

		public void run(){
			if(timer>=totalTime){
				for(Cashier cashier: cashiers){
					if(!cashier.isIdle()){
						cashier.getCustomer().setLeaveTime(timer);
						if(cashier.getCustomer().getServiceTime()>maxServiceTime)
							maxServiceTime=cashier.getCustomer().getServiceTime();
						totalservicetime+=cashier.getCustomer().getServiceTime();
					}
				}
				for(Customer c: customerQueue){
					c.setLeaveTime(timer);
					if(c.getServiceTime()>maxServiceTime)
						maxServiceTime=c.getServiceTime();
					totalservicetime+=c.getServiceTime();
				}
				for(Customer c: customerWaitingPool){
					c.setLeaveTime(timer);
					if(c.getServiceTime()>maxServiceTime)
						maxServiceTime=c.getServiceTime();
					totalservicetime+=c.getServiceTime();
				}
				if(currentCustomerID>1)
					averageservicetime=totalservicetime/(currentCustomerID-1);
				System.out.println("====================Simulation ended======================");
				System.out.println("Max queue length: " + maxQueueLength);
				System.out.println("Max Waiting time(including queueing time): " + maxServiceTime);
				System.out.println("Average Waiting time(including queueing time): " + averageservicetime);
				System.out.println("Max number of customers in waiting area : " + maxWaitLength);
				System.out.println("Cashier idle rate: " + calculateCashierIdleRate());
				System.out.println("Cook idle rate: " + calculateCookIdleRate());
				System.out.println("Barista idle rate: " + calculateBaristaIdleRate());
				log.addevent("====================Simulation ended======================"+"\r\n");
				log.adddata("Max queue length: " + maxQueueLength+"\r\n");
				log.adddata("Max Waiting time(including queueing time): " + maxServiceTime+"\r\n");
				log.adddata("Average waiting time(including queueing time): " + averageservicetime+"\r\n");
				log.adddata("Max number of customers in waiting area : " + maxWaitLength+"\r\n");
				log.adddata("Cashier idle rate: " + calculateCashierIdleRate()+"\r\n");
				log.adddata("Cook idle rate: " + calculateCookIdleRate()+"\r\n");
				log.adddata("Barista idle rate: " + calculateBaristaIdleRate()+"\r\n");
				simtimer.cancel();
				simtimer=null;
				observer.notifyend();
				return;
			}
			timer++;
			// simulate the arrival of customer at time t
			if(timer == nextArrivalTime){
				//create new customer
			   if(queueLength<queueLengthLimit){	
				int ctype = genCustomerType(prepackrate,foodrate,drinkrate,fanddrate); // Defines the customer type
				Customer c = new Customer(currentCustomerID,ctype,timer);
				addNextCustomerToQueue(c);
				queueLength ++;
				updateMaxQueueLength();
				currentCustomerID ++;
				System.out.println(showTime(timer) + ": " + "New incoming customer! Customer ID " + c.getCustomerID() + "   Customer type: " + ctype);
				log.addevent(showTime(timer) + ": " + "New incoming customer! Customer ID " + c.getCustomerID() + "   Customer type: " + ctype+"\r\n");
				System.out.println("Current queue length: " + queueLength);
				log.addevent("Current queue length: " + queueLength+"\r\n");
			   }
			   nextArrivalTime += Math.max( genInterarrivalTime(arrivalRate), 1);
			}
			
			// simulate the work flow of cashiers
			for (Cashier cashier: cashiers){
				
				if(cashier.workDone(timer)){
					System.out.println(showTime(timer) + ": " +"Cashier -- work done! ID: " + cashier.getServingID());
					log.addevent(showTime(timer) + ": " +"Cashier -- work done! ID: " + cashier.getServingID()+"\r\n");
					cashier.endWork();
					idlecashier++;
					if(cashier.getCustomer().getCustomerType()!=0){
					   addOrderToPendingQueue(cashier.getCustomer());
					   addCustomerToWaitingPool(cashier.getCustomer());
					}
					else{
						cashier.getCustomer().setLeaveTime(timer);
						int servicetime=cashier.getCustomer().getServiceTime();
						if(servicetime>maxServiceTime)
							 maxServiceTime=servicetime;
						totalservicetime+=servicetime;
					}
				}
				
				if(cashier.isIdle()){
					if(existPendingCustomer()){
					    Customer c=customerQueue.get(0);
						customerQueue.remove(0);
						System.out.println(showTime(timer) + ": " +"Cashier -- start to work on customer " + c.getCustomerID());
						log.addevent(showTime(timer) + ": " +"Cashier -- start to work on customer " + c.getCustomerID()+"\r\n");
						
						cashier.startWork(c, timer);
						idlecashier--;
						queueLength--;
						System.out.println("Current queue length: " + queueLength);
						log.addevent("Current queue length: " + queueLength+"\r\n");
						//nextCustomerPurchaseID ++;
					    
					}
				}
				
			}
			// simulate the work flow of cooks
			for (Cook cook: cooks){
				
				if(cook.workDone(timer)){
					System.out.println(showTime(timer) + ": " +"Cook -- food order " + cook.getServingID() + " finished");
					log.addevent(showTime(timer) + ": " +"Cook -- food order " + cook.getServingID() + " finished"+"\r\n");
					cook.endWork();
					idlecook++;
					markFoodDone(cook.getFoodProcessing());
				}
				
				if(cook.isIdle()){
					if(existPendingFood()){
						
						Food food = pendingFoodQueue.get(0);
						pendingFoodQueue.remove(0);
						System.out.println(showTime(timer) + ": " +"Cook -- start to work on order " + food.getOrderID());
						log.addevent(showTime(timer) + ": " +"Cook -- start to work on order " + food.getOrderID()+"\r\n");
						cook.startWork(food.getOrderID(), timer, food);
						idlecook--;
					}
				}
				
			}
			
			// simulate the work flow of baristas
			for (Barista barista: baristas){
				
				if(barista.workDone(timer)){
					System.out.println(showTime(timer) + ": " +"Barista -- drink order " + barista.getServingID() + " finished");
					log.addevent(showTime(timer) + ": " +"Barista -- drink order " + barista.getServingID() + " finished"+"\r\n");
					barista.endWork();
					idlebaristas++;
					markDrinkDone(barista.getDrinkProcessing());
				}
				
				if(barista.isIdle()){
					if(existPendingDrink()){
						Drink drink = pendingDrinkQueue.get(0);
						pendingDrinkQueue.remove(0);
						System.out.println(showTime(timer) + ": " +"Barista -- start to work on order " + drink.getOrderID());
						log.addevent(showTime(timer) + ": " +"Barista -- start to work on order " + drink.getOrderID()+"\r\n");
						barista.startWork(drink.getOrderID(), timer, drink);
						idlebaristas--;
					}
				}
				
			}
			// simulate the status of the customer waiting for their order done
			for (Order order: orderQueue){
				if(order.isDone()){
					System.out.println(showTime(timer) + ": " +"=======Order " + order.getOrderID() + " complete=======");
					markOrderDone(order.getOrderID());
					customerLeavePool(order.getOrderID(),timer);
					break;
			    }
			}
			if(observer!=null)
			   if(timer%samplerate==0)
				  observer.notifyupdate(queueLength,customerWaitingPool.size() , idlecashier, idlecook, idlebaristas, showTime(timer));
		
		}
	}
	
	public void start(){
		if(stopped) return;
		Thread simthread=new Thread(this);
		simthread.start();
	}
	public void pause(){
		if(simtimer!=null && !stopped){
		     simtimer.cancel();
		     simtimer=null;
		}
	}
	public void resume(){
		if(simtimer==null && !stopped){
			simtimer=new Timer();
		    simtimer.schedule(new simtask(), 100,100);
		}
	}
	public void stop(){
		
		if(simtimer!=null &&!stopped){
			simtimer.cancel();
			simtimer=null;
		}
		this.stopped=true;
	}
	public String getEvents(){
		return log.getEvent();
	}
	public String getData(){
		return log.getData();
	}
	
	public void save(){
		FileSaver fs=new FileSaver();
		fs.save(log, null);	
	}
	public void setSampleRate(int rate){
		this.samplerate=rate;
	}
	public void run(){
		for(int i=0;i<this.numOfCashier;i++){
			cashiers.add(new Cashier(minCashier,maxCashier));
		}
		for(int i=0;i<this.numOfCook;i++){
			cooks.add(new Cook(minCook,maxCook));
		}
		for(int i=0;i<this.numOfBarista;i++){
			baristas.add(new Barista(minBar,maxBar));
		}
		this.nextArrivalTime = Math.max( genInterarrivalTime(this.arrivalRate), 1); 
		
		// Start simulation
		simtimer.schedule(new simtask(), 100,100);

	
	}

	public void regObeserver(SimObserver obj){
		this.observer=obj;
	}
	
	
	
	/**
	 * Calculate the idle rate of cashiers
	 * @return  The percentage of time that the cashiers are idle
	 */
	private String calculateCookIdleRate() {
		// TODO Auto-generated method stub
		double totalWorkingTime = 0;
		double workingRate;
		double idleRate;
		for (Cook c:cooks){
			totalWorkingTime += c.getWorkingTime();
		}
		workingRate = totalWorkingTime / (this.totalTime * cooks.size());
		idleRate = 1 - workingRate;
		return ( (idleRate*100) + "%" );
	}

	/**
	 * Calculate the idle rate of cooks
	 * @return  The percentage of time that the cooks are idle
	 */
	private String calculateBaristaIdleRate() {
		// TODO Auto-generated method stub
		double totalWorkingTime = 0;
		double workingRate;
		double idleRate;
		for (Barista b:baristas){
			totalWorkingTime += b.getWorkingTime();
		}
		workingRate = totalWorkingTime / (this.totalTime * baristas.size());
		idleRate = 1 - workingRate;
		return ( (idleRate*100) + "%" );
	}
	
	/**
	 * Calculate the idle rate of baristas
	 * @return  The percentage of time that the baristas are idle
	 */
	private String calculateCashierIdleRate() {
		// TODO Auto-generated method stub
		double totalWorkingTime = 0;
		double workingRate;
		double idleRate;
		for (Cashier c:cashiers){
			totalWorkingTime += c.getWorkingTime();
		}
		workingRate = totalWorkingTime / (this.totalTime * cashiers.size());
		idleRate = 1 - workingRate;
		return ( (idleRate*100) + "%" );
	}


	/**
	 *  Update the max queue length after adding a new customer
	 */
	private void updateMaxQueueLength() {
		// TODO Auto-generated method stub
		if(this.queueLength > this.maxQueueLength){
			this.maxQueueLength = this.queueLength;
		}
	}





	/** 
	 * Show if there is customer waiting in main queue
	 * @return true if there is customer waiting in main queue
	 */
	private boolean existPendingCustomer() {
		// TODO Auto-generated method stub
		return (this.customerQueue.size() > 0);
	}





	/**
	 * Remove the order from pending queue
	 * @param orderID  The ID of completed order
	 */
	private void markOrderDone(int orderID) {
		// TODO Auto-generated method stub
		int index = 0;
		boolean flag = false;
		for (Order o: orderQueue){
			if(o.getOrderID() == orderID){
				index = orderQueue.indexOf(o);
				flag = true;
				break;
			}
		}
		if(flag) orderQueue.remove(index);
	}





	/**
	 * Change the order status
	 * @param servingID The ID of drink order
	 */
	private void markDrinkDone(Drink drink) {
		
		// TODO Auto-generated method stub

		for(Order o: orderQueue){
			if(o.getOrderID()==drink.getOrderID()){
				o.setDrinkDone(2);
				break;
			}
		}
	
	}


	/**
	 * Change the order status
	 * @param servingID The ID of the food done
	 */
	private void markFoodDone(Food food) {
		// TODO Auto-generated method stub

		for(Order o: orderQueue){
			if(o.getOrderID()==food.getOrderID()){
				o.setFoodDone(2);
				break;
			}
		}
	}





	/**  
	 * Add an existing customer to main queue
	 * @param customer  The customer we want to add
	 */
	private void addNextCustomerToQueue(Customer customer) {
		// TODO Auto-generated method stub
		this.customerQueue.add(customer);
	}

	
	/**
	 * Generate a new order and pass it to cooks and baristas
	 * @param c references to the customer to add
	 */
	private void addOrderToPendingQueue(Customer c) {
		// TODO Auto-generated method stub
        
		int servingID=c.getCustomerID();
		int ctype = c.getCustomerType();
		this.orderQueue.add(new Order(servingID, ctype));
				// Add pending food or drink
		if(ctype == 1){
					this.pendingFoodQueue.add(new Food(servingID));
		}
		else if(ctype == 2){
					this.pendingDrinkQueue.add(new Drink(servingID));
		}
		else if(ctype == 3){
					this.pendingFoodQueue.add(new Food(servingID));
					this.pendingDrinkQueue.add(new Drink(servingID));
		}	
        		
			
		
	}
	
	/**
	 * After the purchase is finished, add that customer to the waiting pool to wait for their order done
	 * @param c:the target customer
	 */
	private void addCustomerToWaitingPool(Customer c) {
		// TODO Auto-generated method stub
		
		this.customerWaitingPool.add(c);
		if(customerWaitingPool.size()>maxWaitLength)
			maxWaitLength=customerWaitingPool.size();
	}


	/**
	 * After the order is completed, the customer leaves the waiting pool
	 * @param orderID  The ID of the order completed
	 */
	private void customerLeavePool(int orderID,int timer) {
		// TODO Auto-generated method stub
		int index = 0;
		boolean found = false;
		for (Customer c: customerWaitingPool){
			if(c.getCustomerID() == orderID){
				index = customerWaitingPool.indexOf(c);
				found = true;
				// calculate the service time of the customer
				c.setLeaveTime(timer);
				totalservicetime+=c.getServiceTime();
				if(c.getServiceTime() > maxServiceTime){
					maxServiceTime = c.getServiceTime();
				}
				break;
			}
		}
		if(found) {
			this.customerWaitingPool.remove(index);
		}
		
	}


	/**
	 * Show whether there is pending drinks
	 * @return true if there is pending drinks
	 */
	private boolean existPendingDrink() {
		// TODO Auto-generated method stub
		return (this.pendingDrinkQueue.size() > 0);
	}


	/**
	 * Show whether there is pending food
	 * @return true if there is pending food
	 */
	private boolean existPendingFood() {
		// TODO Auto-generated method stub	
	
		
		return (this.pendingFoodQueue.size() > 0);
	}





	/**
	 * Generate the interarrival time of the next customer, which follows a exponential distribution with parameter lambda
	 * @param arrivalRate2  Lambda, the arrival rate specified by the user
	 * @return The interarrival time for the next arrival
	 */
	private int genInterarrivalTime(double arrivalRate2) {
		// TODO Auto-generated method stub
		Random random = new Random();
		return (int) Math.round( -Math.log(random.nextDouble()) / arrivalRate2 );
	}

	private String showTime(int timer){
		int minute = timer / 60;
		int second = timer % 60;
		return String.format("%d:%02d    ",minute,second);
	}
	
	/**
	 * Generate the purchase pattern of the customer 
	 * @param tOne  The percentage of customer that buys packed food
	 * @param tTwo  The percentage of customer that orders food
	 * @param tThree  The percentage of customer that orders drink
	 * @param tFour  The percentage of customer that orders food and drink
	 * @return
	 */
	private int genCustomerType(int tOne, int tTwo, int tThree, int tFour){
		
		Random random = new Random();
		int total = tOne + tTwo + tThree + tFour;
		int i = random.nextInt(total);
		if(i < tOne){
			return 0;
		}
		else if(i < tOne+tTwo){
			return 1;
		}
		else if(i < tOne+tTwo+tThree){
			return 2;
		}
		else {
			return 3;
		}
	}

		

}
